250 votos

Emulando el comportamiento de ajuste de aspecto utilizando las restricciones de AutoLayout en Xcode 6

Quiero utilizar AutoLayout para dimensionar y maquetar una vista de forma que recuerde al modo de contenido de ajuste de aspecto de UIImageView.

Tengo una subvista dentro de una vista contenedora en Interface Builder. La subvista tiene una relación de aspecto inherente que deseo respetar. El tamaño de la vista contenedora se desconoce hasta el momento de la ejecución.

Si la relación de aspecto de la vista contenedora es más ancha que la subvista, entonces quiero que la altura de la subvista sea igual a la altura de la vista padre.

Si la relación de aspecto de la vista contenedora es más alta que la subvista, entonces quiero que el ancho de la subvista sea igual al ancho de la vista padre.

En cualquier caso, deseo que la subvista esté centrada horizontal y verticalmente dentro de la vista contenedora.

¿Existe una manera de lograr esto utilizando las restricciones de AutoLayout en Xcode 6 o en la versión anterior? Lo ideal sería utilizar Interface Builder, pero si no es posible definir estas restricciones mediante programación.

897voto

rob mayoff Puntos 124153

No estás describiendo la escala de ajuste; estás describiendo el aspecto de ajuste. (He editado tu pregunta a este respecto.) La subvista se hace tan grande como sea posible manteniendo su relación de aspecto y encajando completamente dentro de su padre.

De todos modos, se puede hacer esto con la disposición automática. Puedes hacerlo completamente en IB a partir de Xcode 5.1. Vamos a empezar con algunas vistas:

some views

La vista verde claro tiene una relación de aspecto de 4:1. La vista verde oscuro tiene una relación de aspecto de 1:4. Voy a establecer restricciones para que la vista azul llene la mitad superior de la pantalla, la vista rosa llene la mitad inferior de la pantalla, y cada vista verde se expanda tanto como sea posible manteniendo su relación de aspecto y encajando en su contenedor.

Primero, crearé restricciones en los cuatro lados de la vista azul. Lo fijaré a su vecino más cercano en cada borde, con una distancia de 0. Me aseguro de desactivar los márgenes:

blue constraints

Obsérvese que I no actualizar el marco todavía. Me resulta más fácil dejar espacio entre las vistas cuando se establecen las restricciones, y simplemente establecer las constantes a 0 (o lo que sea) a mano.

A continuación, fijo los bordes izquierdo, inferior y derecho de la vista rosa a su vecino más cercano. No necesito establecer una restricción de borde superior porque su borde superior ya está restringido al borde inferior de la vista azul.

pink constraints

También necesito una restricción de alturas iguales entre las vistas rosa y azul. Esto hará que cada una llene la mitad de la pantalla:

enter image description here

Si le digo a Xcode que actualice todos los cuadros ahora, obtengo esto:

containers laid out

Así que las restricciones que he establecido hasta ahora son correctas. Deshago esto y empiezo a trabajar en la vista verde claro.

El ajuste de aspecto de la vista verde claro requiere cinco restricciones:

  • Una restricción de relación de aspecto de prioridad requerida en la vista verde claro. Puedes crear esta restricción en un xib o storyboard con Xcode 5.1 o posterior.
  • Una restricción de prioridad requerida que limita el ancho de la vista verde claro para que sea menor o igual que el ancho de su contenedor.
  • Una restricción de alta prioridad que establece que la anchura de la vista verde claro sea igual a la anchura de su contenedor.
  • Una restricción de prioridad requerida que limita la altura de la vista verde claro para que sea menor o igual a la altura de su contenedor.
  • Una restricción de alta prioridad que establece que la altura de la vista verde claro sea igual a la altura de su contenedor.

Consideremos las dos restricciones de anchura. La restricción de menos que o igual, por sí misma, no es suficiente para determinar el ancho de la vista verde claro; muchos anchos se ajustarán a la restricción. Como hay ambigüedad, el autolayout tratará de elegir una solución que minimice el error en la otra restricción (de alta prioridad pero no requerida). Minimizar el error significa hacer que el ancho sea lo más cercano posible al ancho del contenedor, sin violar la restricción requerida de menos que o igual.

Lo mismo ocurre con la restricción de altura. Y como la restricción de relación de aspecto también es necesaria, sólo puede maximizar el tamaño de la subvista a lo largo de un eje (a menos que el contenedor tenga la misma relación de aspecto que la subvista).

Así que primero creo la restricción de la relación de aspecto:

top aspect

Luego creo restricciones de anchura y altura iguales con el contenedor:

top equal size

Necesito editar estas restricciones para que sean restricciones de menor a igual:

top less than or equal size

A continuación tengo que crear otro conjunto de restricciones de anchura y altura iguales con el contenedor:

top equal size again

Y tengo que hacer que estas nuevas restricciones sean menos prioritarias que las requeridas:

top equal not required

Por último, has pedido que la subvista esté centrada en su contenedor, así que voy a establecer esas restricciones:

top centered

Ahora, para probar, seleccionaré el controlador de vista y pediré a Xcode que actualice todos los fotogramas. Esto es lo que obtengo:

incorrect top layout

¡Uy! La subvista se ha expandido hasta llenar completamente su contenedor. Si la selecciono, puedo ver que de hecho ha mantenido su relación de aspecto, pero está haciendo un aspecto- llenar en lugar de un aspecto- encajar .

El problema es que en una restricción de menos-que-igual, importa qué vista está en cada extremo de la restricción, y Xcode ha configurado la restricción al contrario de lo que esperaba. Podría seleccionar cada una de las dos restricciones e invertir sus primeros y segundos elementos. En lugar de eso, sólo seleccionaré la subvista y cambiaré las restricciones para que sean mayores que o iguales:

fix top constraints

Xcode actualiza el diseño:

correct top layout

Ahora hago todo lo mismo con la vista verde oscura de la parte inferior. Tengo que asegurarme de que su relación de aspecto es 1:4 (Xcode la redimensionó de forma extraña ya que no tenía restricciones). No voy a mostrar los pasos de nuevo ya que son los mismos. Aquí está el resultado:

correct top and bottom layout

Ahora puedo ejecutarlo en el simulador del iPhone 4S, que tiene un tamaño de pantalla diferente al utilizado por IB, y probar la rotación:

iphone 4s test

Y puedo probarlo en el simulador del iPhone 6:

iphone 6 test

He subido mi storyboard final a este punto de vista para su comodidad.

23voto

Johannes Fahrenkrug Puntos 12795

Rob, tu respuesta es impresionante. También sé que esta pregunta es específicamente sobre el logro de esto mediante el uso de auto-diseño. Sin embargo, sólo como referencia, me gustaría mostrar cómo se puede hacer esto en el código. Configuras las vistas superior e inferior (azul y rosa) tal y como mostró Rob. A continuación, crear una costumbre AspectFitView :

AspectFitView.h :

#import <UIKit/UIKit.h>

@interface AspectFitView : UIView

@property (nonatomic, strong) UIView *childView;

@end

AspectFitView.m :

#import "AspectFitView.h"

@implementation AspectFitView

- (void)setChildView:(UIView *)childView
{
    if (_childView) {
        [_childView removeFromSuperview];
    }

    _childView = childView;

    [self addSubview:childView];
    [self setNeedsLayout];
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    if (_childView) {
        CGSize childSize = _childView.frame.size;
        CGSize parentSize = self.frame.size;
        CGFloat aspectRatioForHeight = childSize.width / childSize.height;
        CGFloat aspectRatioForWidth = childSize.height / childSize.width;

        if ((parentSize.height * aspectRatioForHeight) > parentSize.height) {
            // whole height, adjust width
            CGFloat width = parentSize.width * aspectRatioForWidth;
            _childView.frame = CGRectMake((parentSize.width - width) / 2.0, 0, width, parentSize.height);
        } else {
            // whole width, adjust height
            CGFloat height = parentSize.height * aspectRatioForHeight;
            _childView.frame = CGRectMake(0, (parentSize.height - height) / 2.0, parentSize.width, height);
        }
    }
}

@end

A continuación, cambia la clase de las vistas azul y rosa en el storyboard para que sean AspectFitView s. Por último, establece dos salidas en tu viewcontroller topAspectFitView y bottomAspectFitView y fijar su childView s en viewDidLoad :

- (void)viewDidLoad {
    [super viewDidLoad];

    UIView *top = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 100)];
    top.backgroundColor = [UIColor lightGrayColor];

    UIView *bottom = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 500)];
    bottom.backgroundColor = [UIColor greenColor];

    _topAspectFitView.childView = top;
    _bottomAspectFitView.childView = bottom;
}

Así que no es difícil hacer esto en código y sigue siendo muy adaptable y funciona con vistas de tamaño variable y diferentes relaciones de aspecto.

Iteramos.com

Iteramos es una comunidad de desarrolladores que busca expandir el conocimiento de la programación mas allá del inglés.
Tenemos una gran cantidad de contenido, y también puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X